﻿using System;
using System.Collections;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml.Linq;

namespace System.Data
{
    /// <summary>
    /// Provides DataTable functionality
    /// </summary>
    public class DataTable : IDataSource, INotifyPropertyChanged
    {
        #region Static Variables
        /// <summary>
        /// Defines "xs" prefix
        /// </summary>
        private static XNamespace xs = "http://www.w3.org/2001/XMLSchema";
        #endregion

        #region Data Fields
        /// <summary>
        /// "Columns" property data field
        /// </summary>
        private DataColumnCollection _columns;

        /// <summary>
        /// "HasErrors" property data field
        /// </summary>
        private bool _hasErrors;

        /// <summary>
        /// "Rows" property data field
        /// </summary>
        private DataRowCollection _rows;

        /// <summary>
        /// Holds DataSet schema
        /// </summary>
        private XElement _schema;
        #endregion

        #region Events
        /// <summary>
        /// Another row was selected
        /// </summary>
        internal event EventHandler<DataSetEventArgs> AnotherRowWasSelected;

        /// <summary>
        /// INotifyPropertyChanged interface implementation
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region Constructors
        /// <summary>
        /// Default constructor
        /// </summary>
        public DataTable()
        {
            this._columns = new DataColumnCollection();
            this._columns.SchemaChanged += new EventHandler<DataSetEventArgs>(DataColumnCollection_SchemaChanged);
            this._rows = new DataRowCollection(true);
            this._rows.AnotherRowWasSelected += new EventHandler<DataSetEventArgs>(DataRowCollection_AnotherRowWasSelected);
            this._rows.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(DataRowCollection_PropertyChanged);
            this.CreateSchema();
        }

        /// <summary>
        /// Makes a new DataTable instance assigning name to it
        /// </summary>
        /// <param name="tableName">DataTable name</param>
        public DataTable(string tableName) : this()
        {
            // Validate argument
            if (string.IsNullOrEmpty(tableName))
            {
                throw new ArgumentNullException("tableName");
            }

            this.RenameSchema(tableName);
        }

        /// <summary>
        /// Creates DataTable from schema
        /// </summary>
        /// <param name="schema">DataTable schema</param>
        internal DataTable(XElement schema) : this()
        { 
            // Validate argument
            if (schema == null)
            {
                throw new ArgumentNullException("schema");
            }

            this._schema = XElement.Parse(schema.ToString());
            int columnIndex = 0;
            foreach (XElement columnSchema in DataSet.GetColumnSchemas(this._schema))
            {
                DataColumn dataColumn = new DataColumn(columnSchema);
                dataColumn.Index = columnIndex;
                this._columns.Add(dataColumn);
                columnIndex++;
            }
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets columns
        /// </summary>
        public DataColumnCollection Columns
        {
            get
            {
                return this._columns;
            }
        }

        /// <summary>
        /// Gets flag indicating whether DataTable has changes
        /// </summary>
        public bool HasChanges
        {
            get
            {
                for (int i = 0; i < this._rows.Count; i++)
                {
                    if (this._rows[i].HasChanges)
                    {
                        return true;
                    }
                }

                return false;
            }
        }

        /// <summary>
        /// Gets "HasErrors" flag value
        /// </summary>
        public bool HasErrors
        {
            get
            {
                return this._hasErrors;
            }
        }

        /// <summary>
        /// Gets "HasNoErrors" flag value
        /// </summary>
        public bool HasNoErrors
        {
            get
            {
                return !this._hasErrors;
            }
        }

        /// <summary>
        /// Data source name
        /// IDataSource interface implementation
        /// </summary>
        public string Name
        {
            get
            {
                return this._schema.Attribute("name").Value;
            }
        }

        /// <summary>
        /// Gets rows
        /// </summary>
        public DataRowCollection Rows
        {
            get
            {
                return this._rows;
            }
        }

        /// <summary>
        /// Gets schema
        /// </summary>
        internal XElement Schema
        {
            get
            {
                return this._schema;
            }
        }

        /// <summary>
        /// Gets or sets DataTable name
        /// </summary>
        public string TableName
        {
            get
            {
                return this._schema.Attribute("name").Value;
            }
            set
            {
                // Validate argument
                if (string.IsNullOrEmpty(value))
                {
                    throw new ArgumentNullException("value");
                }

                this.RenameSchema(value);
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Commits all the changes made since the last time AcceptChanges was called
        /// </summary>
        public void AcceptChanges()
        {
            for (int i = 0; i < this._rows.Count; i++)
            {
                this._rows[i].AcceptChanges();
            }
        }

        /// <summary>
        /// Finds detail rows for master row
        /// </summary>
        /// <param name="masterDataRows">Master row</param>
        /// <param name="dataRelation">Relation instance</param>
        internal void ApplyDatarelationToDataTable(DataRowCollection masterDataRows, DataRelation dataRelation)
        {
            // Do nothing if master rows don't exist
            if (masterDataRows == null)
            {
                return;
            }

            // Analize every DataRow
            foreach (DataRow dataRow in this._rows)
            {
                // Compare DataRow with every master row
                foreach (DataRow masterDataRow in masterDataRows)
                {
                    int matches = 0;

                    // Compare foreign key values to primary key values
                    for (int i = 0; i < dataRelation.PrimaryKey.Count; i++)
                    {
                        // Finish comparison if foreign key value doesn't match primary key value
                        if (masterDataRow.ItemArray[dataRelation.PrimaryKey[i].ColumnName].Value == dataRow.ItemArray[dataRelation.ForeignKey[i].ColumnName].Value)
                        {
                            matches++;
                        }
                    }

                    if (matches == dataRelation.PrimaryKey.Count)
                    {
                        dataRow.IsChildRow = true;
                    }
                }
            }
        }

        /// <summary>
        /// Copies dataTable structure
        /// </summary>
        /// <param name="addPrefix">Add "CopyOf" prefix to table name</param>
        /// <returns>Empty DataTable</returns>
        public DataTable Clone(bool addPrefix)
        {
            DataTable dataTable = new DataTable();
            dataTable._hasErrors = this._hasErrors;
            if (addPrefix)
            {
                dataTable.TableName = "CopyOf" + this.TableName;
            }
            else
            {
                dataTable.TableName = this.TableName;
            }
            for (int i = 0; i < this._columns.Count; i++)
            {
                dataTable._columns.Add(this._columns[i].Clone());
            }

            return dataTable;
        }

        /// <summary>
        /// Copies DataTable structure and data
        /// </summary>
        /// <param name="addPrefix">Add "CopyOf" prefix to table name</param>
        /// <returns>DataTable copy</returns>
        public IDataSource Copy(bool addPrefix)
        {
            DataTable dataTable = this.Clone(addPrefix);
            for (int i = 0; i < this._rows.Count; i++)
            {
                this._rows[i].Copy(dataTable);
            }

            return dataTable;
        }

        /// <summary>
        /// Creates schema
        /// </summary>
        private void CreateSchema()
        {
            // Load schema
            this._schema = new XElement(xs + "element");
            this._schema.Add(new XAttribute("name", string.Empty));
            XElement complexType = new XElement(xs + "complexType");
            this._schema.Add(complexType);
            XElement sequence = new XElement(xs + "sequence");
            complexType.Add(sequence);
        }

        /// <summary>
        /// Generates bindable objects
        /// </summary>
        /// <param name="connector">Connector instance</param>
        /// <returns>Collection of generated bindable objects</returns>
        public IList GetBindableData(Connector connector)
        {
            BindableObjectCollection dtoCollection = new BindableObjectCollection();
            for (int i = 0; i < this._rows.Count; i++)
            {
                dtoCollection.Add(connector.GetBindableData(this._rows[i], false));
            }

            return dtoCollection;
        }

        /// <summary>
        /// Gets value from specified row / column
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(int rowIndex, string columnName, out bool? value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate object existence
            if (rowIndex >= this._rows.Count)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            DataRow dataRow = this._rows[rowIndex];
            dataRow.GetValue(columnName, out value);
        }

        /// <summary>
        /// Gets value from specified row / column
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(int rowIndex, string columnName, out DateTime? value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate object existence
            if (rowIndex >= this._rows.Count)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            DataRow dataRow = this._rows[rowIndex];
            dataRow.GetValue(columnName, out value);
        }

        /// <summary>
        /// Gets value from specified row / column
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(int rowIndex, string columnName, out Decimal? value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate object existence
            if (rowIndex >= this._rows.Count)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            DataRow dataRow = this._rows[rowIndex];
            dataRow.GetValue(columnName, out value);
        }

        /// <summary>
        /// Gets value from specified row / column
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(int rowIndex, string columnName, out double? value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate object existence
            if (rowIndex >= this._rows.Count)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            DataRow dataRow = this._rows[rowIndex];
            dataRow.GetValue(columnName, out value);
        }

        /// <summary>
        /// Gets value from specified row / column
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(int rowIndex, string columnName, out int? value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate object existence
            if (rowIndex >= this._rows.Count)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            DataRow dataRow = this._rows[rowIndex];
            dataRow.GetValue(columnName, out value);
        }

        /// <summary>
        /// Gets value from specified row / column
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(int rowIndex, string columnName, out long? value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate object existence
            if (rowIndex >= this._rows.Count)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            DataRow dataRow = this._rows[rowIndex];
            dataRow.GetValue(columnName, out value);
        }

        /// <summary>
        /// Gets value from specified row / column
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Output value</param>
        [SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters")]
        public void GetValue(int rowIndex, string columnName, out string value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate object existence
            if (rowIndex >= this._rows.Count)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            DataRow dataRow = this._rows[rowIndex];
            dataRow.GetValue(columnName, out value);
        }

        /// <summary>
        /// Creates new DataRow
        /// </summary>
        /// <returns>New DataRow</returns>
        public DataRow NewRow()
        {
            return new DataRow(this);
        }

        /// <summary>
        /// Raises PropertChanged event
        /// </summary>
        /// <param name="propertyName">Property name</param>
        private void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        /// <summary>
        /// Recalculates table errors
        /// </summary>
        private void RecalculateErrors()
        {
            // Prepare result variable
            bool hasErrors = false;

            // Check all rows
            for (int j = 0; j < this._rows.Count; j++)
            {
                if (this._rows[j].HasErrors)
                {
                    hasErrors = true;
                    break;
                }
            }

            // Change "HasErrors" property if needed
            if (this._hasErrors != hasErrors)
            {
                this._hasErrors = hasErrors;
                this.OnPropertyChanged("HasErrors");
                this.OnPropertyChanged("HasNoErrors");
            }
        }

        /// <summary>
        /// Rejects all changes made since AcceptChanges was last called
        /// </summary>
        public void RejectChanges()
        {
            for (int i = 0; i < this._rows.Count; i++)
            {
                this._rows[i].RejectChanges();
            }
        }

        /// <summary>
        /// Renames schema
        /// </summary>
        /// <param name="dataSetName">DataSet name</param>
        private void RenameSchema(string dataSetName)
        {
            this._schema.Attribute("name").Value = dataSetName;
        }

        /// <summary>
        /// Sets or resets IsChildRow flag
        /// </summary>
        /// <param name="value">IsChildRow flag value</param>
        internal void SetChildRows(bool value)
        {
            for (int i = 0; i < this._rows.Count; i++)
            {
                this._rows[i].IsChildRow = value;
            }
        }

        /// <summary>
        /// Sets value into specified table / row / filed
        /// Creates row if table has no rows or row index equals row count
        /// Throws an exception if table has rows but row index frate than row count
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnIndex">Column index</param>
        /// <param name="value">Value for assigning</param>
        internal void SetValue(int rowIndex, int columnIndex, object value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            // Validate DataColumn existense
            DataColumn dataColumn = this._columns[columnIndex];
            if (dataColumn == null)
            {
                throw new ArgumentOutOfRangeException("columnIndex");
            }

            // Validate row existense
            bool newRow = false;
            DataRow dataRow = null;
            if (this._rows.Count == 0)
            {
                dataRow = this.NewRow();
                newRow = true;
            }
            else
            {
                if (rowIndex == this._rows.Count)
                {
                    dataRow = this.NewRow();
                    newRow = true;
                }
                else
                {
                    if (rowIndex > this._rows.Count)
                    {
                        throw new ArgumentOutOfRangeException("rowIndex");
                    }
                    else
                    {
                        dataRow = this._rows[rowIndex];
                    }
                }
            }

            if (value != null)
            {
                dataRow.SetValue(columnIndex, value);
            }

            if (newRow)
            {
                this._rows.Add(dataRow);
            }
        }

        /// <summary>
        /// Sets value into specified row / filed
        /// Throws an exception if table has rows but row index frate than row count
        /// Creates column if it doesn't exist
        /// </summary>
        /// <param name="rowIndex">Row index</param>
        /// <param name="columnName">Column name</param>
        /// <param name="value">Value for assigning</param>
        /// <param name="valueType">Value type</param>
        public void SetValue(int rowIndex, string columnName, object value)
        {
            // Validate arguments
            if (rowIndex < 0)
            {
                throw new ArgumentOutOfRangeException("rowIndex");
            }

            if (String.IsNullOrEmpty(columnName))
            {
                throw new ArgumentNullException("columnName");
            }

            // Validate DataColumn existense
            DataColumn dataColumn = this._columns[columnName];
            if (dataColumn == null)
            {
                dataColumn = new DataColumn(columnName, value.GetType());
                this._columns.Add(dataColumn);
            }

            // Validate row existense
            bool newRow = false;
            DataRow dataRow = null;
            if (this._rows.Count == 0)
            {
                dataRow = this.NewRow();
                newRow = true;
            }
            else
            {
                if (rowIndex == this._rows.Count)
                {
                    dataRow = this.NewRow();
                    newRow = true;
                }
                else
                {
                    if (rowIndex > this._rows.Count)
                    {
                        throw new ArgumentOutOfRangeException("rowIndex");
                    }
                    else
                    {
                        dataRow = this.Rows[rowIndex];
                    }
                }
            }

            dataRow.SetValue(columnName, value);

            if (newRow)
            {
                this._rows.Add(dataRow);
            }
        }
        #endregion

        #region Event Handlers
        /// <summary>
        /// Handles DataColumnCollection.DataColumnRemoving event
        /// </summary>
        /// <param name="sender">Event source</param>
        /// <param name="e">Event arguments</param>
        private void DataColumnCollection_SchemaChanged(object sender, DataSetEventArgs e)
        {
            switch (e.Command)
            {
                case "Add":
                    // Add column schema to table schema
                    XElement columnSchema = DataSet.GetColumnSchema(this._schema, e.Column.ColumnName);
                    if (columnSchema == null)
                    {
                        this._schema.Element(xs + "complexType").Element(xs + "sequence").Add(e.Column.Schema);
                    }

                    // Add fields to all rows
                    for (int i = 0; i < this._rows.Count; i++)
                    {
                        this._rows[i].AddDataField(e.Column.Index);
                    }

                    break;
                case "ChangeType":
                    for (int i = 0; i < this._rows.Count; i++)
                    {
                        this._rows[i].ItemArray[e.Column.ColumnName].ChangeType(e.ProposedValue);
                    }

                    break;
                case "NullValue":
                    for (int i = 0; i < this._rows.Count; i++)
                    {
                        this._rows[i].ItemArray[e.Column.ColumnName].RecalculateErrors();
                    }

                    break;
                case "Remove":
                    // Remove column schema from table schema
                    e.Column.Schema.Remove();

                    // Remove fields from all rows
                    for (int i = 0; i < this._rows.Count; i++)
                    {
                        this._rows[i].Remove(e.Column.ColumnName);
                    }

                    break;
            }
        }

        /// <summary>
        /// Handles DataRowCollection.AnotherRowWasSelected event
        /// </summary>
        /// <param name="sender">Event source</param>
        /// <param name="e">Event arguments</param>
        void DataRowCollection_AnotherRowWasSelected(object sender, DataSetEventArgs e)
        {
            DataRowCollection dataRowCollection = (DataRowCollection)sender;
            for (int i = 0; i < dataRowCollection.Count; i++)
            {
                // If last edited row in Edit mode
                if (dataRowCollection[i].InEditMode)
                {
                    // Set current row as last edited
                    dataRowCollection[i].EndEdit();
                }
            }

            // Pass AnotherRowWasSelected event to DataSet for handling
            if (AnotherRowWasSelected != null)
            {
                AnotherRowWasSelected(sender, e);
            }
        }

        /// <summary>
        /// Handles DataRowCollection.PropertyChanged event
        /// </summary>
        /// <param name="sender">Event source</param>
        /// <param name="e">Event arguments</param>
        void DataRowCollection_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            if (e.PropertyName.Contains("HasError"))
            {
                this.RecalculateErrors();
            }
            else
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, e);
                }
            }
        }
        #endregion

        public void Load(IDataReader dr)
        {
            CopyData(dr.GetTable());
        }


        private void CopyData(DataTable dt)
        {
            this._schema = dt._schema; // XElement.Parse(schema.ToString());
            foreach (DataColumn col in dt._columns)
            {
                DataColumn dataColumn = new DataColumn(col.Schema);
                dataColumn.Index = col.Index;
                this._columns.Add(dataColumn);
            }

            foreach (DataRow row in dt._rows)
            {
                DataRow dataRow = this.NewRow();
                foreach ( DataField field in row.ItemArray)
                {
                    //int index = int.Parse(field.Name.LocalName.Substring(1), CultureInfo.InvariantCulture);
                   // DataField dataField = dataRow.ItemArray[index];
                    //dataField.UseCurrentCulture = false;
                    dataRow[field.ColumnName] = field.Value;
                }

                for (int j = 0; j < dataRow.ItemArray.Count; j++)
                {
                    dataRow.ItemArray[j].UseCurrentCulture = true;
                }

               this.Rows.Add(dataRow);
            }
        }

        internal void Dispose()
        {
            this.Rows.Clear();
            //this.Rows = null;
            this.Columns.Clear();
            //this.Columns = null;

        }

        public void Clear()
        {
        }
    }
}
